/**
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 *
 * Version: 5.10.0 (2021-10-11)
 */
(function() {
  'use strict'

  var Cell = function(initial) {
    var value = initial
    var get = function() {
      return value
    }
    var set = function(v) {
      value = v
    }
    return {
      get: get,
      set: set
    }
  }

  var global$3 = tinymce.util.Tools.resolve('tinymce.PluginManager')

  var hasProPlugin = function(editor) {
    if (editor.hasPlugin('tinymcespellchecker', true)) {
      if (typeof window.console !== 'undefined' && window.console.log) {
        window.console.log('Spell Checker Pro is incompatible with Spell Checker plugin! ' + 'Remove \'spellchecker\' from the \'plugins\' option.')
      }
      return true
    } else {
      return false
    }
  }

  var hasOwnProperty = Object.hasOwnProperty
  var isEmpty = function(r) {
    for (var x in r) {
      if (hasOwnProperty.call(r, x)) {
        return false
      }
    }
    return true
  }

  var global$2 = tinymce.util.Tools.resolve('tinymce.util.Tools')

  var global$1 = tinymce.util.Tools.resolve('tinymce.util.URI')

  var global = tinymce.util.Tools.resolve('tinymce.util.XHR')

  var fireSpellcheckStart = function(editor) {
    return editor.fire('SpellcheckStart')
  }
  var fireSpellcheckEnd = function(editor) {
    return editor.fire('SpellcheckEnd')
  }

  var getLanguages = function(editor) {
    var defaultLanguages = 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,German=de,Italian=it,Polish=pl,Portuguese=pt_BR,Spanish=es,Swedish=sv'
    return editor.getParam('spellchecker_languages', defaultLanguages)
  }
  var getLanguage = function(editor) {
    var defaultLanguage = editor.getParam('language', 'en')
    return editor.getParam('spellchecker_language', defaultLanguage)
  }
  var getRpcUrl = function(editor) {
    return editor.getParam('spellchecker_rpc_url')
  }
  var getSpellcheckerCallback = function(editor) {
    return editor.getParam('spellchecker_callback')
  }
  var getSpellcheckerWordcharPattern = function(editor) {
    var defaultPattern = new RegExp('[^' + '\\s!"#$%&()*+,-./:;<=>?@[\\]^_{|}`' + '\xA7\xA9\xAB\xAE\xB1\xB6\xB7\xB8\xBB' + '\xBC\xBD\xBE\xBF\xD7\xF7\xA4\u201D\u201C\u201E\xA0\u2002\u2003\u2009' + ']+', 'g')
    return editor.getParam('spellchecker_wordchar_pattern', defaultPattern)
  }

  var isContentEditableFalse = function(node) {
    return node && node.nodeType === 1 && node.contentEditable === 'false'
  }
  var DomTextMatcher = function(node, editor) {
    var m;var matches = []
    var dom = editor.dom
    var blockElementsMap = editor.schema.getBlockElements()
    var hiddenTextElementsMap = editor.schema.getWhiteSpaceElements()
    var shortEndedElementsMap = editor.schema.getShortEndedElements()
    var createMatch = function(m, data) {
      if (!m[0]) {
        throw new Error('findAndReplaceDOMText cannot handle zero-length matches')
      }
      return {
        start: m.index,
        end: m.index + m[0].length,
        text: m[0],
        data: data
      }
    }
    var getText = function(node) {
      if (node.nodeType === 3) {
        return node.data
      }
      if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
        return ''
      }
      if (isContentEditableFalse(node)) {
        return '\n'
      }
      var txt = ''
      if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
        txt += '\n'
      }
      if (node = node.firstChild) {
        do {
          txt += getText(node)
        } while (node = node.nextSibling)
      }
      return txt
    }
    var stepThroughMatches = function(node, matches, replaceFn) {
      var startNode;var endNode;var startNodeIndex;var endNodeIndex;var innerNodes = [];var atIndex = 0;var curNode = node;var matchLocation;var matchIndex = 0
      matches = matches.slice(0)
      matches.sort(function(a, b) {
        return a.start - b.start
      })
      matchLocation = matches.shift()
      out:
      while (true) {
        if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
          atIndex++
        }
        if (curNode.nodeType === 3) {
          if (!endNode && curNode.length + atIndex >= matchLocation.end) {
            endNode = curNode
            endNodeIndex = matchLocation.end - atIndex
          } else if (startNode) {
            innerNodes.push(curNode)
          }
          if (!startNode && curNode.length + atIndex > matchLocation.start) {
            startNode = curNode
            startNodeIndex = matchLocation.start - atIndex
          }
          atIndex += curNode.length
        }
        if (startNode && endNode) {
          curNode = replaceFn({
            startNode: startNode,
            startNodeIndex: startNodeIndex,
            endNode: endNode,
            endNodeIndex: endNodeIndex,
            innerNodes: innerNodes,
            match: matchLocation.text,
            matchIndex: matchIndex
          })
          atIndex -= endNode.length - endNodeIndex
          startNode = null
          endNode = null
          innerNodes = []
          matchLocation = matches.shift()
          matchIndex++
          if (!matchLocation) {
            break
          }
        } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
          if (!isContentEditableFalse(curNode)) {
            curNode = curNode.firstChild
            continue
          }
        } else if (curNode.nextSibling) {
          curNode = curNode.nextSibling
          continue
        }
        while (true) {
          if (curNode.nextSibling) {
            curNode = curNode.nextSibling
            break
          } else if (curNode.parentNode !== node) {
            curNode = curNode.parentNode
          } else {
            break out
          }
        }
      }
    }
    var genReplacer = function(callback) {
      var makeReplacementNode = function(fill, matchIndex) {
        var match = matches[matchIndex]
        if (!match.stencil) {
          match.stencil = callback(match)
        }
        var clone = match.stencil.cloneNode(false)
        clone.setAttribute('data-mce-index', '' + matchIndex)
        if (fill) {
          clone.appendChild(dom.doc.createTextNode(fill))
        }
        return clone
      }
      return function(range) {
        var before
        var after
        var parentNode
        var startNode = range.startNode
        var endNode = range.endNode
        var matchIndex = range.matchIndex
        var doc = dom.doc
        if (startNode === endNode) {
          var node_1 = startNode
          parentNode = node_1.parentNode
          if (range.startNodeIndex > 0) {
            before = doc.createTextNode(node_1.data.substring(0, range.startNodeIndex))
            parentNode.insertBefore(before, node_1)
          }
          var el = makeReplacementNode(range.match, matchIndex)
          parentNode.insertBefore(el, node_1)
          if (range.endNodeIndex < node_1.length) {
            after = doc.createTextNode(node_1.data.substring(range.endNodeIndex))
            parentNode.insertBefore(after, node_1)
          }
          node_1.parentNode.removeChild(node_1)
          return el
        }
        before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex))
        after = doc.createTextNode(endNode.data.substring(range.endNodeIndex))
        var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex)
        for (var i = 0, l = range.innerNodes.length;i < l;++i) {
          var innerNode = range.innerNodes[i]
          var innerEl = makeReplacementNode(innerNode.data, matchIndex)
          innerNode.parentNode.replaceChild(innerEl, innerNode)
        }
        var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex)
        parentNode = startNode.parentNode
        parentNode.insertBefore(before, startNode)
        parentNode.insertBefore(elA, startNode)
        parentNode.removeChild(startNode)
        parentNode = endNode.parentNode
        parentNode.insertBefore(elB, endNode)
        parentNode.insertBefore(after, endNode)
        parentNode.removeChild(endNode)
        return elB
      }
    }
    var unwrapElement = function(element) {
      var parentNode = element.parentNode
      while (element.childNodes.length > 0) {
        parentNode.insertBefore(element.childNodes[0], element)
      }
      parentNode.removeChild(element)
    }
    var hasClass = function(elm) {
      return elm.className.indexOf('mce-spellchecker-word') !== -1
    }
    var getWrappersByIndex = function(index) {
      var elements = node.getElementsByTagName('*');var wrappers = []
      index = typeof index === 'number' ? '' + index : null
      for (var i = 0;i < elements.length;i++) {
        var element = elements[i];var dataIndex = element.getAttribute('data-mce-index')
        if (dataIndex !== null && dataIndex.length && hasClass(element)) {
          if (dataIndex === index || index === null) {
            wrappers.push(element)
          }
        }
      }
      return wrappers
    }
    var indexOf = function(match) {
      var i = matches.length
      while (i--) {
        if (matches[i] === match) {
          return i
        }
      }
      return -1
    }
    function filter(callback) {
      var filteredMatches = []
      each(function(match, i) {
        if (callback(match, i)) {
          filteredMatches.push(match)
        }
      })
      matches = filteredMatches
      return this
    }
    function each(callback) {
      for (var i = 0, l = matches.length;i < l;i++) {
        if (callback(matches[i], i) === false) {
          break
        }
      }
      return this
    }
    function wrap(callback) {
      if (matches.length) {
        stepThroughMatches(node, matches, genReplacer(callback))
      }
      return this
    }
    function find(regex, data) {
      if (text && regex.global) {
        while (m = regex.exec(text)) {
          matches.push(createMatch(m, data))
        }
      }
      return this
    }
    function unwrap(match) {
      var i
      var elements = getWrappersByIndex(match ? indexOf(match) : null)
      i = elements.length
      while (i--) {
        unwrapElement(elements[i])
      }
      return this
    }
    var matchFromElement = function(element) {
      return matches[element.getAttribute('data-mce-index')]
    }
    var elementFromMatch = function(match) {
      return getWrappersByIndex(indexOf(match))[0]
    }
    function add(start, length, data) {
      matches.push({
        start: start,
        end: start + length,
        text: text.substr(start, length),
        data: data
      })
      return this
    }
    var rangeFromMatch = function(match) {
      var wrappers = getWrappersByIndex(indexOf(match))
      var rng = editor.dom.createRng()
      rng.setStartBefore(wrappers[0])
      rng.setEndAfter(wrappers[wrappers.length - 1])
      return rng
    }
    var replace = function(match, text) {
      var rng = rangeFromMatch(match)
      rng.deleteContents()
      if (text.length > 0) {
        rng.insertNode(editor.dom.doc.createTextNode(text))
      }
      return rng
    }
    function reset() {
      matches.splice(0, matches.length)
      unwrap()
      return this
    }
    var text = getText(node)
    return {
      text: text,
      matches: matches,
      each: each,
      filter: filter,
      reset: reset,
      matchFromElement: matchFromElement,
      elementFromMatch: elementFromMatch,
      find: find,
      add: add,
      wrap: wrap,
      unwrap: unwrap,
      replace: replace,
      rangeFromMatch: rangeFromMatch,
      indexOf: indexOf
    }
  }

  var getTextMatcher = function(editor, textMatcherState) {
    if (!textMatcherState.get()) {
      var textMatcher = DomTextMatcher(editor.getBody(), editor)
      textMatcherState.set(textMatcher)
    }
    return textMatcherState.get()
  }
  var defaultSpellcheckCallback = function(editor, pluginUrl, currentLanguageState) {
    return function(method, text, doneCallback, errorCallback) {
      var data = {
        method: method,
        lang: currentLanguageState.get()
      }
      var postData = ''
      data[method === 'addToDictionary' ? 'word' : 'text'] = text
      global$2.each(data, function(value, key) {
        if (postData) {
          postData += '&'
        }
        postData += key + '=' + encodeURIComponent(value)
      })
      global.send({
        url: new global$1(pluginUrl).toAbsolute(getRpcUrl(editor)),
        type: 'post',
        content_type: 'application/x-www-form-urlencoded',
        data: postData,
        success: function(result) {
          var parseResult = JSON.parse(result)
          if (!parseResult) {
            var message = editor.translate('Server response wasn\'t proper JSON.')
            errorCallback(message)
          } else if (parseResult.error) {
            errorCallback(parseResult.error)
          } else {
            doneCallback(parseResult)
          }
        },
        error: function() {
          var message = editor.translate('The spelling service was not found: (') + getRpcUrl(editor) + editor.translate(')')
          errorCallback(message)
        }
      })
    }
  }
  var sendRpcCall = function(editor, pluginUrl, currentLanguageState, name, data, successCallback, errorCallback) {
    var userSpellcheckCallback = getSpellcheckerCallback(editor)
    var spellCheckCallback = userSpellcheckCallback || defaultSpellcheckCallback(editor, pluginUrl, currentLanguageState)
    spellCheckCallback.call(editor.plugins.spellchecker, name, data, successCallback, errorCallback)
  }
  var spellcheck = function(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState) {
    if (finish(editor, startedState, textMatcherState)) {
      return
    }
    var errorCallback = function(message) {
      editor.notificationManager.open({
        text: message,
        type: 'error'
      })
      editor.setProgressState(false)
      finish(editor, startedState, textMatcherState)
    }
    var successCallback = function(data) {
      markErrors(editor, startedState, textMatcherState, lastSuggestionsState, data)
    }
    editor.setProgressState(true)
    sendRpcCall(editor, pluginUrl, currentLanguageState, 'spellcheck', getTextMatcher(editor, textMatcherState).text, successCallback, errorCallback)
    editor.focus()
  }
  var checkIfFinished = function(editor, startedState, textMatcherState) {
    if (!editor.dom.select('span.mce-spellchecker-word').length) {
      finish(editor, startedState, textMatcherState)
    }
  }
  var addToDictionary = function(editor, pluginUrl, startedState, textMatcherState, currentLanguageState, word, spans) {
    editor.setProgressState(true)
    sendRpcCall(editor, pluginUrl, currentLanguageState, 'addToDictionary', word, function() {
      editor.setProgressState(false)
      editor.dom.remove(spans, true)
      checkIfFinished(editor, startedState, textMatcherState)
    }, function(message) {
      editor.notificationManager.open({
        text: message,
        type: 'error'
      })
      editor.setProgressState(false)
    })
  }
  var ignoreWord = function(editor, startedState, textMatcherState, word, spans, all) {
    editor.selection.collapse()
    if (all) {
      global$2.each(editor.dom.select('span.mce-spellchecker-word'), function(span) {
        if (span.getAttribute('data-mce-word') === word) {
          editor.dom.remove(span, true)
        }
      })
    } else {
      editor.dom.remove(spans, true)
    }
    checkIfFinished(editor, startedState, textMatcherState)
  }
  var finish = function(editor, startedState, textMatcherState) {
    var bookmark = editor.selection.getBookmark()
    getTextMatcher(editor, textMatcherState).reset()
    editor.selection.moveToBookmark(bookmark)
    textMatcherState.set(null)
    if (startedState.get()) {
      startedState.set(false)
      fireSpellcheckEnd(editor)
      return true
    }
  }
  var getElmIndex = function(elm) {
    var value = elm.getAttribute('data-mce-index')
    if (typeof value === 'number') {
      return '' + value
    }
    return value
  }
  var findSpansByIndex = function(editor, index) {
    var spans = []
    var nodes = global$2.toArray(editor.getBody().getElementsByTagName('span'))
    if (nodes.length) {
      for (var i = 0;i < nodes.length;i++) {
        var nodeIndex = getElmIndex(nodes[i])
        if (nodeIndex === null || !nodeIndex.length) {
          continue
        }
        if (nodeIndex === index.toString()) {
          spans.push(nodes[i])
        }
      }
    }
    return spans
  }
  var markErrors = function(editor, startedState, textMatcherState, lastSuggestionsState, data) {
    var hasDictionarySupport = !!data.dictionary
    var suggestions = data.words
    editor.setProgressState(false)
    if (isEmpty(suggestions)) {
      var message = editor.translate('No misspellings found.')
      editor.notificationManager.open({
        text: message,
        type: 'info'
      })
      startedState.set(false)
      return
    }
    lastSuggestionsState.set({
      suggestions: suggestions,
      hasDictionarySupport: hasDictionarySupport
    })
    var bookmark = editor.selection.getBookmark()
    getTextMatcher(editor, textMatcherState).find(getSpellcheckerWordcharPattern(editor)).filter(function(match) {
      return !!suggestions[match.text]
    }).wrap(function(match) {
      return editor.dom.create('span', {
        'class': 'mce-spellchecker-word',
        'aria-invalid': 'spelling',
        'data-mce-bogus': 1,
        'data-mce-word': match.text
      })
    })
    editor.selection.moveToBookmark(bookmark)
    startedState.set(true)
    fireSpellcheckStart(editor)
  }

  var get = function(editor, startedState, lastSuggestionsState, textMatcherState, currentLanguageState) {
    var getWordCharPattern = function() {
      return getSpellcheckerWordcharPattern(editor)
    }
    var markErrors$1 = function(data) {
      markErrors(editor, startedState, textMatcherState, lastSuggestionsState, data)
    }
    return {
      getTextMatcher: textMatcherState.get,
      getWordCharPattern: getWordCharPattern,
      markErrors: markErrors$1,
      getLanguage: currentLanguageState.get
    }
  }

  var register$1 = function(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState) {
    editor.addCommand('mceSpellCheck', function() {
      spellcheck(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState)
    })
  }

  var __assign = function() {
    __assign = Object.assign || function __assign(t) {
      for (var s, i = 1, n = arguments.length;i < n;i++) {
        s = arguments[i]
        for (var p in s) {
          if (Object.prototype.hasOwnProperty.call(s, p)) { t[p] = s[p] }
        }
      }
      return t
    }
    return __assign.apply(this, arguments)
  }

  var spellcheckerEvents = 'SpellcheckStart SpellcheckEnd'
  var buildMenuItems = function(listName, languageValues) {
    var items = []
    global$2.each(languageValues, function(languageValue) {
      items.push({
        selectable: true,
        text: languageValue.name,
        data: languageValue.value
      })
    })
    return items
  }
  var getItems = function(editor) {
    return global$2.map(getLanguages(editor).split(','), function(langPair) {
      var langPairs = langPair.split('=')
      return {
        name: langPairs[0],
        value: langPairs[1]
      }
    })
  }
  var register = function(editor, pluginUrl, startedState, textMatcherState, currentLanguageState, lastSuggestionsState) {
    var languageMenuItems = buildMenuItems('Language', getItems(editor))
    var startSpellchecking = function() {
      spellcheck(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState)
    }
    var buttonArgs = {
      tooltip: 'Spellcheck',
      onAction: startSpellchecking,
      icon: 'spell-check',
      onSetup: function(buttonApi) {
        var setButtonState = function() {
          buttonApi.setActive(startedState.get())
        }
        editor.on(spellcheckerEvents, setButtonState)
        return function() {
          editor.off(spellcheckerEvents, setButtonState)
        }
      }
    }
    var splitButtonArgs = __assign(__assign({}, buttonArgs), {
      type: 'splitbutton',
      select: function(value) {
        return value === currentLanguageState.get()
      },
      fetch: function(callback) {
        var items = global$2.map(languageMenuItems, function(languageItem) {
          return {
            type: 'choiceitem',
            value: languageItem.data,
            text: languageItem.text
          }
        })
        callback(items)
      },
      onItemAction: function(splitButtonApi, value) {
        currentLanguageState.set(value)
      }
    })
    if (languageMenuItems.length > 1) {
      editor.ui.registry.addSplitButton('spellchecker', splitButtonArgs)
    } else {
      editor.ui.registry.addToggleButton('spellchecker', buttonArgs)
    }
    editor.ui.registry.addToggleMenuItem('spellchecker', {
      text: 'Spellcheck',
      icon: 'spell-check',
      onSetup: function(menuApi) {
        menuApi.setActive(startedState.get())
        var setMenuItemCheck = function() {
          menuApi.setActive(startedState.get())
        }
        editor.on(spellcheckerEvents, setMenuItemCheck)
        return function() {
          editor.off(spellcheckerEvents, setMenuItemCheck)
        }
      },
      onAction: startSpellchecking
    })
  }

  var ignoreAll = true
  var getSuggestions = function(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, currentLanguageState, word, spans) {
    var items = []
    var suggestions = lastSuggestionsState.get().suggestions[word]
    global$2.each(suggestions, function(suggestion) {
      items.push({
        text: suggestion,
        onAction: function() {
          editor.insertContent(editor.dom.encode(suggestion))
          editor.dom.remove(spans)
          checkIfFinished(editor, startedState, textMatcherState)
        }
      })
    })
    var hasDictionarySupport = lastSuggestionsState.get().hasDictionarySupport
    if (hasDictionarySupport) {
      items.push({ type: 'separator' })
      items.push({
        text: 'Add to dictionary',
        onAction: function() {
          addToDictionary(editor, pluginUrl, startedState, textMatcherState, currentLanguageState, word, spans)
        }
      })
    }
    items.push.apply(items, [
      { type: 'separator' },
      {
        text: 'Ignore',
        onAction: function() {
          ignoreWord(editor, startedState, textMatcherState, word, spans)
        }
      },
      {
        text: 'Ignore all',
        onAction: function() {
          ignoreWord(editor, startedState, textMatcherState, word, spans, ignoreAll)
        }
      }
    ])
    return items
  }
  var setup = function(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, currentLanguageState) {
    var update = function(element) {
      var target = element
      if (target.className === 'mce-spellchecker-word') {
        var spans = findSpansByIndex(editor, getElmIndex(target))
        if (spans.length > 0) {
          var rng = editor.dom.createRng()
          rng.setStartBefore(spans[0])
          rng.setEndAfter(spans[spans.length - 1])
          editor.selection.setRng(rng)
          return getSuggestions(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, currentLanguageState, target.getAttribute('data-mce-word'), spans)
        }
      } else {
        return []
      }
    }
    editor.ui.registry.addContextMenu('spellchecker', { update: update })
  }

  function Plugin() {
    global$3.add('spellchecker', function(editor, pluginUrl) {
      if (hasProPlugin(editor) === false) {
        var startedState = Cell(false)
        var currentLanguageState = Cell(getLanguage(editor))
        var textMatcherState = Cell(null)
        var lastSuggestionsState = Cell(null)
        register(editor, pluginUrl, startedState, textMatcherState, currentLanguageState, lastSuggestionsState)
        setup(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, currentLanguageState)
        register$1(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState)
        return get(editor, startedState, lastSuggestionsState, textMatcherState, currentLanguageState)
      }
    })
  }

  Plugin()
}())
